package com.gwtintegration.smartrf.server;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Entity;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.transform.ResultTransformer;

import com.gwtintegration.smartrf.shared.DataRequestOperatorId;
import com.gwtintegration.smartrf.shared.DataRequestSortDirection;
import com.gwtintegration.smartrf.shared.RequestFactoryEncoder;

public final class DataRequestQuery {

	public interface DataRequestCriterionBuilder {
		Criterion buildCriterion(DataRequestQuery dataRequestQuery, DataRequestCriterion filter);
	}

	private final DataRequest dataRequest;

	private final Criteria criteria;

	private final Map<String, String> aliasMap;

	private Set<String> ignoredFilters;

	private final Map<String, DataRequestCriterionBuilder> criterionBuilders;

	private final Class<?> entityClass;

	private Projection projection;
	private ResultTransformer resultTransformer;

	private boolean filterBuilded;

	public DataRequestQuery(DataRequest dataRequest, Class<?> entityClass, Session session) {
		this.dataRequest = dataRequest;
		this.entityClass = entityClass;

		this.criteria = session.createCriteria(entityClass);

		this.aliasMap = new HashMap<String, String>();
		this.criterionBuilders = new HashMap<String, DataRequestCriterionBuilder>();
	}

	public Integer getResultCount() {

		buildCriterion();

		criteria.setProjection(Projections.rowCount());
		return ((Number) criteria.uniqueResult()).intValue();
	}

	@SuppressWarnings("rawtypes")
	public List getResultList() {

		buildCriterion();

		criteria.setProjection(projection);
		if (resultTransformer == null) {
			criteria.setResultTransformer(Criteria.ROOT_ENTITY);
		} else {
			criteria.setResultTransformer(resultTransformer);
		}

		buildOrder();

		if (dataRequest.getStartRow() != null) {
			criteria.setFirstResult(dataRequest.getStartRow());
		}
		if (dataRequest.getEndRow() != null) {
			criteria.setMaxResults(dataRequest.getEndRow());
		}

		return criteria.list();
	}

	@SuppressWarnings("unchecked")
	public <R extends DataResponse<?>> R getResult(R dataResult) {

		dataResult.setTotalRows(getResultCount());

		if (dataResult.getTotalRows().equals(0)) {
			return dataResult;
		}

		dataResult.setContent(getResultList());

		return dataResult;
	}

	private void buildCriterion() {
		if (!filterBuilded) {
			filterBuilded = true;
			if (dataRequest.getCriteria() != null) {
				Criterion criterion = buildCriterion(dataRequest.getCriteria());
				if (criterion != null) {
					criteria.add(criterion);
				}
			}
		}
	}

	private Criterion buildCriterion(DataRequestCriterion filter) {
		if (filter != null) {

			DataRequestCriterionBuilder criterionBuilder = criterionBuilders.get(filter.getFieldName());

			if (criterionBuilder != null) {
				return criterionBuilder.buildCriterion(this, filter);
			} else {
				if (filter.getCriterions() != null && !filter.getCriterions().isEmpty()) {

					Junction junction = null;
					switch (filter.getOperator()) {
						case AND:
							junction = Restrictions.conjunction();
							break;
						case OR:
							junction = Restrictions.disjunction();
							break;
						default:
							throw new UnsupportedOperationException("The operation " + filter.getOperator()
									+ " is unsupported for aggregate sub filters.");
					}

					for (DataRequestCriterion subFilter : filter.getCriterions()) {
						Criterion criterion = buildCriterion(subFilter);
						if (criterion != null) {
							junction.add(criterion);
						}
					}

					return junction;

				} else if (filter.getFieldName() != null && !isFilterIgnored(filter.getFieldName()) && !filter.getFieldName().isEmpty()) {

					FieldContext fieldContext = getFieldContext(filter.getFieldName());

					if (filter.getValue() == null) {
						if (filter.getOperator() == DataRequestOperatorId.IS_NULL) {
							return Restrictions.isNull(fieldContext.getFilterPath());
						} else {
							return Restrictions.isNotNull(fieldContext.getFilterPath());
						}
					}

					Object value = RequestFactoryEncoder.getObject(filter.getValue());

					switch (filter.getOperator()) {
						case EQUALS:
							return Restrictions.eq(fieldContext.getFilterPath(), value);
						case IEQUALS:
							return Restrictions.eq(fieldContext.getFilterPath(), value).ignoreCase();
						case NOT_EQUAL:
							return Restrictions.ne(fieldContext.getFilterPath(), value);
						case INOT_EQUAL:
							return Restrictions.ne(fieldContext.getFilterPath(), value).ignoreCase();
						case IS_NULL:
							return Restrictions.isNull(fieldContext.getFilterPath());
						case NOT_NULL:
							return Restrictions.isNotNull(fieldContext.getFilterPath());
						case STARTS_WITH:
							return Restrictions.like(fieldContext.getFilterPath(), value.toString(), MatchMode.START);
						case ISTARTS_WITH:
							return Restrictions.ilike(fieldContext.getFilterPath(), value.toString(), MatchMode.START);
						case ENDS_WITH:
							return Restrictions.like(fieldContext.getFilterPath(), value.toString(), MatchMode.END);
						case IENDS_WITH:
							return Restrictions.ilike(fieldContext.getFilterPath(), value.toString(), MatchMode.END);
						case CONTAINS:
							return Restrictions.like(fieldContext.getFilterPath(), value.toString(), MatchMode.ANYWHERE);
						case ICONTAINS:
							return Restrictions.ilike(fieldContext.getFilterPath(), value.toString(), MatchMode.ANYWHERE);
						default:
							throw new UnsupportedOperationException("The operation " + filter.getOperator() + " is not supported.");
					}
				}
			}
		}

		return null;
	}

	private void buildOrder() {
		if (dataRequest.getSortBy() != null && !dataRequest.getSortBy().isEmpty()) {
			for (DataRequestSortSpecifier sort : dataRequest.getSortBy()) {

				FieldContext fieldContext = getFieldContext(sort.getFieldName());

				if (sort.getDirection() == DataRequestSortDirection.ASCENDING) {
					criteria.addOrder(Order.asc(fieldContext.getFilterPath()));
				} else {
					criteria.addOrder(Order.desc(fieldContext.getFilterPath()));
				}
			}
		}
	}

	public FieldContext getFieldContext(String fieldName) {
		String propertyPath = fieldName;
		if (propertyPath == null || propertyPath.isEmpty()) {
			return null;
		}

		String[] properties = propertyPath.split("\\.");
		if (properties == null || properties.length == 0) {
			return null;
		}

		StringBuilder currentPropertyPath = new StringBuilder();
		StringBuilder simplePropertyPath = new StringBuilder();

		Class<?> entityType = entityClass;
		Class<?> clazz = entityClass;
		String parentAlias = "this";

		for (String property : properties) {

			if (currentPropertyPath.length() > 0) {
				currentPropertyPath.append('.');
			}

			currentPropertyPath.append(property);

			Class<?> fieldType = getDeclaredFieldType(clazz, property);
			if (fieldType == null) {
				throw new IllegalArgumentException("Can not found field " + property + " inside class " + clazz.getName() + ".");
			}

			if (fieldType.isAnnotationPresent(Entity.class)) {

				entityType = fieldType;

				if (!aliasMap.containsKey(currentPropertyPath.toString())) {

					String alias = createAliasName(property);
					criteria.createAlias(parentAlias + "." + property, alias);

					aliasMap.put(currentPropertyPath.toString(), alias);
					parentAlias = alias;

				} else {
					parentAlias = aliasMap.get(currentPropertyPath.toString());
				}

				if (simplePropertyPath.length() > 0) {
					simplePropertyPath = new StringBuilder();
				}

			} else {
				if (simplePropertyPath.length() > 0) {
					simplePropertyPath.append('.');
				}
				simplePropertyPath.append(property);
			}

			clazz = fieldType;
		}

		return new FieldContext(parentAlias, entityType, simplePropertyPath.toString());
	}

	private Class<?> getDeclaredFieldType(Class<?> clazz, String fieldName) {
		while (!Object.class.equals(clazz)) {

			try {
				Field field = clazz.getDeclaredField(fieldName);

				if (field != null) {
					if (Collection.class.isAssignableFrom(field.getType()) && field.getGenericType() instanceof ParameterizedType) {
						ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
						return (Class<?>) parameterizedType.getActualTypeArguments()[0];
					}

					return field.getType();
				}

			} catch (Exception e) {}

			clazz = clazz.getSuperclass();
		}

		return null;
	}

	public DataRequestQuery setCriterionBuilder(String fieldName, DataRequestCriterionBuilder criterionBuilder) {

		if (criterionBuilder == null) {
			criterionBuilders.remove(fieldName);
		} else {
			criterionBuilders.put(fieldName, criterionBuilder);
		}

		return this;
	}

	public DataRequestQuery addIgnoredFilter(String fieldName) {

		if (fieldName != null) {
			if (ignoredFilters == null) {
				ignoredFilters = new HashSet<String>();
			}

			ignoredFilters.add(fieldName);
		}

		return this;
	}

	public DataRequestQuery removeIgnoredFilter(String fieldName) {
		if (ignoredFilters != null) {
			ignoredFilters.remove(fieldName);
		}

		return this;
	}

	public boolean isFilterIgnored(String fieldName) {
		return ignoredFilters != null && ignoredFilters.contains(fieldName);
	}

	public String createAliasName(String property) {
		return property + "_" + aliasMap.size();
	}

	public DataRequest getDataRequest() {
		return dataRequest;
	}

	public Criteria getCriteria() {
		return criteria;
	}

	public Projection getProjection() {
		return projection;
	}

	public DataRequestQuery setProjection(Projection projection) {
		this.projection = projection;
		return this;
	}

	public ResultTransformer getResultTransformer() {
		return resultTransformer;
	}

	public DataRequestQuery setResultTransformer(ResultTransformer resultTransformer) {
		this.resultTransformer = resultTransformer;
		return this;
	}

	public static class FieldContext {

		private String parentAlias;

		private Class<?> entity;

		private String fieldName;

		public FieldContext() {}

		public FieldContext(String parentAlias, Class<?> entity, String fieldName) {
			this.parentAlias = parentAlias;
			this.entity = entity;
			this.fieldName = fieldName;
		}

		public String getParentAlias() {
			return parentAlias;
		}

		public void setParentAlias(String parentAlias) {
			this.parentAlias = parentAlias;
		}

		public Class<?> getEntityClass() {
			return entity;
		}

		public void setEntity(Class<?> entity) {
			this.entity = entity;
		}

		public String getFieldName() {
			return fieldName;
		}

		public void setFieldName(String fieldName) {
			this.fieldName = fieldName;
		}

		public String getFilterPath() {
			return parentAlias + "." + fieldName;
		}
	}
}
